#!/usr/bin/perl

# An ncurses-based git branch switcher. Useful when you have to switch branch frequently

use strict;
use Curses;
use Time::HiRes;

my $recent_file = "$ENV{HOME}/.git_recent_branches";
my @branches;
my @recent_branches;
my $current_search;
my $currsel=0;
my $postcmd;
update_branches();

initscr();
start_color();
init_pair(1, COLOR_BLACK, COLOR_WHITE); #Select cursor
init_pair(2, COLOR_GREEN, COLOR_BLACK); #Recent branches
keypad(1);
mainloop();
endwin;

if( $postcmd && $postcmd->{cmd} eq 'branch' ) {
	my $changebranch = $postcmd->{val};
	print "Changing branch to $changebranch\n";
	if( $changebranch =~ /^remotes\/origin\/(.*)$/ ) {
		system("git fetch origin $1");
		if( $? ) {
			print "git-branch.pl: fail\n";
			exit;
		}
		$changebranch = $1;
	}elsif( $changebranch =~ /^\*\s(.*)$/ ) {
		$changebranch = $1;
	}
	
	system("git checkout $changebranch");
	if( $? ) {
		print "git-branch.pl: fail\n";
		exit;
	}

	if( open(F, ">$recent_file") ) {
		print F join("\n", reverse @recent_branches)."\n$changebranch\n";
		close(F);
	}else{
		warn "Cant write to $recent_file: $!";
	}
}
elsif( $postcmd && $postcmd->{cmd} eq 'log' ) {
	system("git log $postcmd->{val}");
}
elsif( $postcmd && $postcmd->{cmd} eq 'merge' ) {
	$postcmd->{val} =~ s/^remotes\///;
	system("git merge $postcmd->{val}");
}

sub update_branches() {
	my %seen=();
	if( open(F, $recent_file) ) {
		@recent_branches = <F>;
		chomp(@recent_branches);
		@recent_branches = grep { $_ && !$seen{$_}++ } reverse @recent_branches;
		close(F);
	}
	@branches = `git branch -a`;
	if( $? ) {
		print "'git branch -a' command failed!\n";
		exit;
	}
	foreach my $b (@branches) {
		chomp($b);
		$b =~ s/^\s*(?:\*\s*)?||\s+$//g;
		$b = undef if $seen{$b}++;
	}
	@branches = grep { $_ } @branches;
}

sub mainloop() {
while(1) {
	my $br = [@recent_branches, @branches];
	my $last_recent = $recent_branches[-1];
	if( $current_search ) {
		$br = limitgrep( $Curses::LINES, sub {
				index($_[0], $current_search)>=0 || eval { $_[0] =~ m/$current_search/; }
			}, $br);
	}

	clear;
	my $l = scalar(@$br);
	$l=$Curses::LINES-4 if $l > ($Curses::LINES-4);
	if( $currsel>=$l ) {
		$currsel = $l-1;
	}
	for(my $i=0; $i<$l; $i++) {
		if( $i == $currsel ) {
			attron(COLOR_PAIR(1));
		}elsif( $last_recent ) {
			attron(COLOR_PAIR(2));
		}
		if( $last_recent eq $br->[$i] ) {
			undef $last_recent;
		}
		addstr($i+2, 4, $br->[$i]);
		attroff(COLOR_PAIR(1));
		attroff(COLOR_PAIR(2));
	}
	addstr(0, 0, $current_search);
	refresh;
	Time::HiRes::usleep(1_000);
	my $k = Curses::getch();
	$k = KEY_BACKSPACE if ( ord($k) == 127 or $k eq "\cH" );
	if( $k eq KEY_UP ) {
		$currsel--;
		$currsel=0 if $currsel<0;
	}elsif( $k eq KEY_DOWN ) {
		$currsel++;
	}elsif( $k eq "\n" || $k eq "\027" ) {
		$postcmd = { cmd => 'branch', val => $br->[$currsel] };
		return;
	}elsif( $k eq '\cR' || ord($k) == 0x12 ) { #control-r
		$postcmd = { cmd => 'log', val => $br->[$currsel] };
		return;
	}elsif( $k eq '\cE' || ord($k) == 5 ) { #control-e
		$postcmd = { cmd => 'merge', val => $br->[$currsel] };
		return;
	}elsif( $k eq KEY_BACKSPACE ) {
		substr($current_search, -1) = '';
	}elsif( length($k) == 1 ) {
		$current_search .= $k;
	}
}
}

sub limitgrep {
	my ($lim, $checksub, $arr) = @_;
	my $res=[];
	my $c=0;
	foreach my $a (@$arr) {
		if( $checksub->($a) ) {
			push @$res, $a;
			last if $c++ >= $lim;
		}
	}
	return $res;
}
